package com.atguigu.gmall.gateway.filter;

import com.atguigu.gmall.common.consts.RedisConst;
import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.IpUtil;
import com.atguigu.gmall.common.util.JSONStr;
import com.atguigu.gmall.gateway.properties.AuthProperties;
import com.atguigu.gmall.model.user.UserInfo;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * @Author 卡尼奶
 * @Date 2022/5/31 13:56
 * @Version 1.0
 */
@Component
@Slf4j
public class UserAuthFilter implements GlobalFilter {

    @Autowired
    private AuthProperties authProperties;

    private AntPathMatcher matcher = new AntPathMatcher();

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        //获取访问路径
        String path = request.getPath().toString();
        log.info("网关拦截路径：{}", path);
        List<String> anyoneurls = authProperties.getAnyoneurls();
        for (String anyoneurl : anyoneurls) {
            boolean match = matcher.match(anyoneurl, path);
            if (match) {
                //任何人都可以访问，静态资源
                return chain.filter(exchange);
            }
        }

        //登录后也不允许访问内部Feign调用接口
        List<String> declineurls = authProperties.getDeclineurls();
        for (String declineurl : declineurls) {
            boolean match = matcher.match(declineurl, path);
            if (match) {
                //登录后也不允许访问，返回JSON数据
                Result<String> result = Result.build("", ResultCodeEnum.FORBIDDEN);
                String jsonStr = JSONStr.toJsonStr(result);
                ServerHttpResponse response = exchange.getResponse();
                DataBuffer buffer = response.bufferFactory().wrap(jsonStr.getBytes(StandardCharsets.UTF_8));
                //发布者
                Publisher<? extends DataBuffer> publisher = Mono.just(buffer);
                //解决响应乱码
                response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
                return response.writeWith(publisher);
            }
        }
        //登录后才能访问
        List<String> authurls = authProperties.getAuthurls();
        for (String authurl : authurls) {
            boolean match = matcher.match(authurl, path);
            if (match) {
                //检查前端是否携带token
                boolean flag = verifyToken(request);
                if (!flag) {
                    return toLoginPage(exchange, request);

                }
            }
        }

        String token = getToken(request);
        //如果没有带token，放行
        if (StringUtils.isEmpty(token)) {

            //获取用户临时Id
            String tempId = getTempId(request);
            //把临时ID透传
            ServerHttpRequest build = exchange.getRequest()
                    .mutate()
                    .header("UserTempId", tempId)
                    .build(); //克隆一个请求

            //克隆一个exchange
            ServerWebExchange serverWebExchange = exchange.mutate()
                    .request(build)
                    .response(exchange.getResponse())
                    .build();

            return chain.filter(exchange);
        } else {
            //如果携带了token，并验证是否正确
            boolean verifyToken = verifyToken(request);
            if (verifyToken) {
                //如果token正确，则在请求头添加 UserId
                UserInfo userInfo = isToken(token, request);
                //获取用户临时Id
                String tempId = getTempId(request);
                ServerHttpRequest build = exchange.getRequest()
                        .mutate()
                        .header("UserId", userInfo.getId().toString())
                        .header("UserTempId", tempId)
                        .build(); //克隆一个请求

                //克隆一个exchange
                ServerWebExchange serverWebExchange = exchange.mutate()
                        .request(build)
                        .response(exchange.getResponse())
                        .build();
                return chain.filter(serverWebExchange);
            } else {
                //如果token错误，重定向登录页
                return toLoginPage(exchange, request);
            }
        }
    }

    /**
     * 获取用户临时ID
     * @param request
     * @return
     */
    private String getTempId(ServerHttpRequest request) {
        HttpCookie cookieToken = request.getCookies().getFirst("userTempId");
        String userTempId = "";
        if (cookieToken == null) {
            //去请求头 是否有token
            userTempId = request.getHeaders().getFirst("userTempId");
        } else {
            userTempId = cookieToken.getValue();
        }
        return userTempId;
    }

    private Mono<Void> toLoginPage(ServerWebExchange exchange, ServerHttpRequest request) {
        //未登录
        ServerHttpResponse response = exchange.getResponse();
        //设置状态码 重定向302
        response.setStatusCode(HttpStatus.FOUND);
        String url = request.getURI().toString();
        //设置重定向位置
        response.getHeaders().setLocation(URI.create(authProperties.getLoginurl() + "?originUrl=" + url));

        //销毁假令牌，设置过期时间
        ResponseCookie cookie = ResponseCookie.from("token", "gmall")
                .maxAge(0L).domain(".gmall.com").build();
        response.addCookie(cookie);

        return response.setComplete();
    }

    /**
     * 获取Token信息
     *
     * @param request
     * @return
     */
    private String getToken(ServerHttpRequest request) {
        HttpCookie cookieToken = request.getCookies().getFirst("token");
        String token = "";
        if (cookieToken == null) {
            //去请求头 是否有token
            token = request.getHeaders().getFirst("token");
        } else {
            token = cookieToken.getValue();
        }
        return token;
    }

    /**
     * 判断前端是否携带token信息
     *
     * @param request
     * @return
     */
    private boolean verifyToken(ServerHttpRequest request) {
        HttpCookie cookieToken = request.getCookies().getFirst("token");
        String token = "";
        if (cookieToken == null) {
            //去请求头 是否有token
            token = request.getHeaders().getFirst("token");
        } else {
            token = cookieToken.getValue();
        }

        if (!StringUtils.isEmpty(token)) {
            //如果不为空，则判断token是否正确
            UserInfo info = isToken(token, request);
            if (info != null) {
                return true;
            }
            return false;
        }
        return false;
    }

    /**
     * 验证token在redis中是否正确
     *
     * @param token
     * @param request
     * @return
     */
    private UserInfo isToken(String token, ServerHttpRequest request) {
        //从redis中查询
        String redisToken = redisTemplate.opsForValue().get(RedisConst.USER_LOGIN_PREFIX + token);
        if (StringUtils.isEmpty(redisToken)) {
            return null;
        } else {
            //把JSON数据转为对象
            UserInfo userInfo = JSONStr.strToObj(redisToken, new TypeReference<UserInfo>() {
            });
            //获取访问者IP
            String ipAddress = IpUtil.getGatwayIpAddress(request);
            //判断如果访问者IP和redis存储的ip不一致，则返回null
            if (!userInfo.getIpAddr().equals(ipAddress)) {
                return null;
            }
            return userInfo;
        }
    }
}
